import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import threading
from selenium.webdriver.support.wait import WebDriverWait
from seleniumwire import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
import time
import requests
import csv
import os
import json
import random


class BossZhipinScraper:
    def __init__(self, root):
        self.root = root
        self.root.title("BOSS直聘数据爬取工具")
        self.root.geometry("600x500")

        self.driver = None
        self.is_running = False
        self.user_agent = ""
        self.cookie = ""

        self.setup_ui()

    def setup_ui(self):
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # 输入区域
        input_frame = ttk.LabelFrame(main_frame, text="爬取参数", padding="10")
        input_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)

        # 职位关键词
        ttk.Label(input_frame, text="职位关键词:").grid(row=0, column=0, sticky=tk.W, pady=5)
        self.job_var = tk.StringVar(value="bi")
        self.job_entry = ttk.Entry(input_frame, textvariable=self.job_var, width=30)
        self.job_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)

        # 城市ID
        ttk.Label(input_frame, text="城市ID:").grid(row=1, column=0, sticky=tk.W, pady=5)
        self.city_var = tk.StringVar(value="100010000")
        self.city_entry = ttk.Entry(input_frame, textvariable=self.city_var, width=30)
        self.city_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)

        # 页数设置 - 改为起始页和结束页
        ttk.Label(input_frame, text="起始页:").grid(row=2, column=0, sticky=tk.W, pady=5)
        self.start_page_var = tk.StringVar(value="1")
        self.start_page_entry = ttk.Entry(input_frame, textvariable=self.start_page_var, width=30)
        self.start_page_entry.grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)

        ttk.Label(input_frame, text="结束页:").grid(row=3, column=0, sticky=tk.W, pady=5)
        self.end_page_var = tk.StringVar(value="10")
        self.end_page_entry = ttk.Entry(input_frame, textvariable=self.end_page_var, width=30)
        self.end_page_entry.grid(row=3, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)

        # 模拟操作设置
        ttk.Label(input_frame, text="模拟操作间隔(秒):").grid(row=4, column=0, sticky=tk.W, pady=5)
        self.interval_var = tk.StringVar(value="3-10")
        self.interval_entry = ttk.Entry(input_frame, textvariable=self.interval_var, width=30)
        self.interval_entry.grid(row=4, column=1, sticky=(tk.W, tk.E), pady=5, padx=5)

        # 按钮区域
        btn_frame = ttk.Frame(main_frame)
        btn_frame.grid(row=1, column=0, columnspan=2, pady=10)

        self.start_btn = ttk.Button(btn_frame, text="开始爬取", command=self.start_scraping)
        self.start_btn.pack(side=tk.LEFT, padx=5)

        self.stop_btn = ttk.Button(btn_frame, text="停止爬取", command=self.stop_scraping, state=tk.DISABLED)
        self.stop_btn.pack(side=tk.LEFT, padx=5)

        # 日志区域
        log_frame = ttk.LabelFrame(main_frame, text="运行日志", padding="10")
        log_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)

        self.log_text = scrolledtext.ScrolledText(log_frame, height=15, width=70)
        self.log_text.pack(fill=tk.BOTH, expand=True)

        # 进度条
        self.progress = ttk.Progressbar(main_frame, mode='determinate')
        self.progress.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)

        # 状态标签
        self.status_var = tk.StringVar(value="准备就绪")
        status_label = ttk.Label(main_frame, textvariable=self.status_var)
        status_label.grid(row=4, column=0, columnspan=2, pady=5)

        # 配置网格权重
        main_frame.columnconfigure(0, weight=1)
        input_frame.columnconfigure(1, weight=1)
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)

    def log(self, message):
        """在日志区域添加消息"""
        self.log_text.insert(tk.END, f"{time.strftime('%H:%M:%S')} - {message}\n")
        self.log_text.see(tk.END)
        self.root.update_idletasks()

    def update_status(self, message):
        """更新状态"""
        self.status_var.set(message)
        self.log(message)

    def get_updated_cookie(self):
        """获取最新的cookie"""
        if not self.driver:
            return self.cookie

        cookies = self.driver.get_cookies()
        cookie_strings = []
        for cookie in cookies:
            cookie_strings.append(f"{cookie['name']}={cookie['value']}")
        cookie_str = '; '.join(cookie_strings)
        print(f'cookie更新: {cookie_str}')
        return cookie_str

    def simulate_human_behavior(self):
        """模拟人类操作行为"""
        if not self.driver:
            return

        # 记录操作前的窗口句柄
        current_window = self.driver.current_window_handle
        try:
            # 随机选择一种操作
            actions = [
                self.simulate_scrolling,
                # self.simulate_mouse_movement,
                self.simulate_input_behavior,
                self.simulate_random_clicks,
                self.simulate_tab_switching
            ]

            action = random.choice(actions)
            action()

        except Exception as e:
            self.log(f"模拟操作时出错: {str(e)}")
        finally:
            # 确保切换回操作前的窗口
            try:
                self.driver.switch_to.window(current_window)
            except Exception as e:
                self.log(f"切换回窗口失败: {str(e)}")

    def simulate_scrolling(self):
        """模拟页面滚动"""
        self.log("模拟页面滚动...")

        # 随机滚动模式
        scroll_pattern = random.choice(["smooth", "quick", "step"])

        if scroll_pattern == "smooth":
            # 平滑滚动
            for i in range(random.randint(3, 8)):
                scroll_height = random.randint(200, 800)
                scroll_direction = random.choice([-1, 1])
                self.driver.execute_script(f"window.scrollBy(0, {scroll_height * scroll_direction});")
                time.sleep(random.uniform(0.5, 1.5))

        elif scroll_pattern == "quick":
            # 快速滚动到底部或顶部
            if random.choice([True, False]):
                self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            else:
                self.driver.execute_script("window.scrollTo(0, 0);")

        else:  # step
            # 分步滚动
            total_scrolls = random.randint(2, 5)
            for _ in range(total_scrolls):
                scroll_amount = random.randint(300, 700)
                self.driver.execute_script(f"window.scrollBy(0, {scroll_amount});")
                time.sleep(random.uniform(0.3, 0.8))


    def simulate_input_behavior(self):
        """模拟输入行为"""
        self.log("模拟输入行为...")

        try:
            # 尝试找到搜索框或其他输入框
            input_selectors = [
                "input[type='text']",
                "input[placeholder*='搜索']",
                "input[placeholder*='Search']",
                ".search-input",
                "#search",
                "input[name='keyword']"
            ]

            for selector in input_selectors:
                try:
                    input_element = self.driver.find_element(By.CSS_SELECTOR, selector)
                    if input_element.is_displayed():
                        actions = ActionChains(self.driver)

                        # 点击输入框
                        actions.click(input_element)
                        actions.pause(random.uniform(0.5, 1))

                        # 随机输入一些内容
                        test_inputs = ["python", "java", "数据分析", "开发工程师", "测试", "产品经理", "运营"]
                        input_text = random.choice(test_inputs)

                        # 模拟逐个字符输入
                        for char in input_text:
                            actions.send_keys(char)
                            actions.pause(random.uniform(0.05, 0.2))

                        # 随机决定是否清除输入
                        if random.random() < 0.3:
                            actions.pause(random.uniform(0.5, 1))
                            actions.key_down(Keys.CONTROL).send_keys('a').key_up(Keys.CONTROL)
                            actions.pause(random.uniform(0.2, 0.5))
                            actions.send_keys(Keys.BACKSPACE)

                        actions.perform()
                        break

                except:
                    continue

        except Exception as e:
            self.log(f"输入行为模拟失败: {str(e)}")

    def simulate_random_clicks(self):
        """模拟随机点击（安全区域）"""
        self.log("模拟随机点击...")

        try:
            # 只点击安全的元素类型
            safe_selectors = [
                "div", "span", "button:not(.danger)",
                "a[href*='javascript']", ".card", ".item"
            ]

            all_elements = []
            for selector in safe_selectors:
                try:
                    elements = self.driver.find_elements(By.CSS_SELECTOR, selector)
                    # 过滤可见且可点击的元素
                    visible_elements = [el for el in elements if el.is_displayed() and el.is_enabled()]
                    all_elements.extend(visible_elements)
                except:
                    continue

            if all_elements:
                # 随机选择几个元素进行点击
                for _ in range(random.randint(1, 3)):
                    element = random.choice(all_elements)
                    try:
                        # 滚动到元素可见
                        self.driver.execute_script(
                            "arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", element)
                        time.sleep(random.uniform(0.5, 1))

                        # 点击元素
                        element.click()
                        time.sleep(random.uniform(20, 40))

                        # 50%概率按回退键返回
                        if random.random() < 0.5:
                            self.driver.back()
                            time.sleep(random.uniform(1, 2))

                    except:
                        continue

        except Exception as e:
            self.log(f"随机点击模拟失败: {str(e)}")

    def simulate_tab_switching(self):
        """模拟标签页切换"""
        self.log("模拟标签页切换...")

        try:
            # 获取所有窗口句柄
            handles = self.driver.window_handles
            if len(handles) > 1:
                # 切换到其他标签页
                switch_to = random.choice([h for h in handles if h != self.driver.current_window_handle])
                self.driver.switch_to.window(switch_to)
                time.sleep(random.uniform(2, 4))
                # 切换回来
                self.driver.switch_to.window(handles[0])
            else:
                # 如果没有其他标签页，打开一个新的
                self.driver.execute_script("window.open('https://www.zhipin.com/web/geek/job');")
                time.sleep(random.uniform(2, 4))
                # 关闭新标签页并切换回原标签页
                self.driver.switch_to.window(handles[0])

        except Exception as e:
            self.log(f"标签页切换模拟失败: {str(e)}")

    def perform_random_operation(self):
        """执行随机操作"""
        if not self.driver:
            return

        # 解析间隔时间
        interval_str = self.interval_var.get()
        if '-' in interval_str:
            min_interval, max_interval = map(float, interval_str.split('-'))
            interval = random.uniform(min_interval, max_interval)
        else:
            interval = float(interval_str)

        self.log(f"等待 {interval:.1f} 秒后执行模拟操作...")
        time.sleep(interval)
        # 执行模拟操作
        self.simulate_human_behavior()

    def login_boss_zhiping(self):
        """登录并获取初始cookie"""
        try:
            service = Service("D:\chrome\chromedriver-win64\chromedriver.exe")
            options = {
                'request_storage': 'memory',
                'request_storage_max_size': 100
            }
            self.driver = webdriver.Chrome(service=service, seleniumwire_options=options)

            self.update_status("正在打开登录页面...")
            self.driver.get("https://www.zhipin.com/web/user")

            self.update_status("请在弹出的浏览器窗口中完成登录...")
            WebDriverWait(self.driver, 50).until(EC.presence_of_element_located((By.CLASS_NAME, "link-logout")))

            self.update_status("登录成功！")
            time.sleep(60)
            # 登录成功后进行一些模拟操作
            self.log("开始模拟用户行为...")
            for _ in range(random.randint(2, 4)):
                if not self.is_running:
                    break
                self.perform_random_operation()

            self.user_agent = self.driver.execute_script("return navigator.userAgent")
            self.cookie = self.get_updated_cookie()

            print(f"User-Agent: {self.user_agent}")
            print(f"Cookie获取成功")
            return True

        except Exception as e:
            self.update_status(f"登录失败: {str(e)}")
            return False

    def extract_job_info(self, data):
        """提取职位信息"""
        job_list = data.get('zpData', {}).get('jobList', [])
        extracted_data = []

        for job in job_list:
            job_info = {
                '职位名称': job.get('jobName', ''),
                '薪资': job.get('salaryDesc', ''),
                '工作经验': job.get('jobExperience', ''),
                '学历要求': job.get('jobDegree', ''),
                '城市': job.get('cityName', ''),
                '区域': job.get('areaDistrict', ''),
                '商圈': job.get('businessDistrict', ''),
                '技能要求': ', '.join(job.get('skills', [])),
                '公司名称': job.get('brandName', ''),
                '行业': job.get('brandIndustry', ''),
                '公司规模': job.get('brandScaleName', '')
            }
            extracted_data.append(job_info)
        return extracted_data

    def save_to_csv(self, job_data, filename=None):
        """将职位数据保存到CSV文件"""
        if not filename:
            job_name = self.job_var.get()
            filename = f'{job_name}_职位数据.csv'

        file_exists = os.path.isfile(filename)
        with open(filename, 'a', newline='', encoding='utf-8-sig') as csvfile:
            fieldnames = ['职位名称', '薪资', '工作经验', '学历要求', '城市', '区域', '商圈',
                          '技能要求', '公司名称', '行业', '公司规模']
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            if not file_exists:
                writer.writeheader()
            for job in job_data:
                writer.writerow(job)
        return filename

    def make_request_with_retry(self, url, headers, max_retries=3):
        """带重试机制的请求函数"""
        for attempt in range(max_retries):
            if not self.is_running:
                return None

            try:
                current_timestamp = str(int(time.time() * 1000))
                if '&_=' in url:
                    base_url = url.split('&_=')[0]
                    url_with_timestamp = f"{base_url}&_={current_timestamp}"
                else:
                    url_with_timestamp = f"{url}&_={current_timestamp}"

                self.log(f"第 {attempt + 1} 次尝试请求")
                print(url_with_timestamp)
                response = requests.get(url_with_timestamp, headers=headers, timeout=10)

                if response.status_code == 200:
                    data = response.json()
                    if data.get('code') == 0:
                        return data
                    else:
                        error_msg = data.get('message', '未知错误')
                        self.log(f"API返回错误: {error_msg}")

                        if "访问行为异常" in error_msg or "异常" in error_msg:
                            wait_time = (attempt + 1) * 10
                            self.log(f"检测到异常，等待 {wait_time} 秒后重试...")
                            time.sleep(wait_time)
                            continue
                else:
                    self.log(f"请求失败，状态码: {response.status_code}")

            except requests.exceptions.RequestException as e:
                self.log(f"请求异常: {e}")

            if attempt < max_retries - 1:
                delay = random.uniform(20, 40)
                self.log(f"等待 {delay:.2f} 秒后重试...")
                time.sleep(delay)

        self.log(f"请求失败，已达最大重试次数 {max_retries}")
        return None

    def scraping_thread(self):
        """爬取线程"""
        try:
            # 登录
            if not self.login_boss_zhiping():
                return

            # 设置请求头
            base_headers = {
                'User-Agent': self.user_agent,
                'Referer': 'https://www.zhipin.com/web/geek/job',
                'Accept': 'application/json, text/plain, */*',
                'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
                'Connection': 'keep-alive',
            }

            job_name = self.job_var.get()
            city_id = self.city_var.get()

            # 获取起始页和结束页
            try:
                start_page = int(self.start_page_var.get())
                end_page = int(self.end_page_var.get())
            except ValueError:
                self.update_status("页码格式错误，请确保输入的是数字")
                return

            if start_page < 1:
                start_page = 1
                self.log("起始页不能小于1，已自动设置为1")

            if end_page < start_page:
                self.update_status("结束页不能小于起始页")
                return

            total_pages = end_page - start_page + 1

            self.progress['maximum'] = total_pages
            successful_pages = 0

            self.log(f"开始爬取第 {start_page} 页到第 {end_page} 页，共 {total_pages} 页")

            for page_num in range(start_page, end_page + 1):
                if not self.is_running:
                    break

                current_progress = page_num - start_page + 1
                self.update_status(f"正在爬取第 {page_num} 页... (进度: {current_progress}/{total_pages})")

                # 在每次请求前随机进行模拟操作
                if random.random() < 0.7:  # 70%的概率在请求前进行模拟操作
                    self.perform_random_operation()

                # 更新cookie
                try:
                    updated_cookie = self.get_updated_cookie()
                    if updated_cookie:
                        self.cookie = updated_cookie  # 更新全局cookie
                    headers = base_headers.copy()
                    headers['Cookie'] = self.cookie
                    self.log("Cookie更新成功")
                except Exception as e:
                    self.log(f"更新Cookie失败，使用旧Cookie: {e}")
                    headers = base_headers.copy()
                    headers['Cookie'] = self.cookie

                current_timestamp = str(int(time.time() * 1000))
                url = f"https://www.zhipin.com/wapi/zpgeek/search/joblist.json?page={page_num}&pageSize=30&query={job_name}&city={city_id}&scene=1&_={current_timestamp}"

                data = self.make_request_with_retry(url, headers)

                if data is not None:
                    job_info = self.extract_job_info(data)
                    if job_info:
                        filename = self.save_to_csv(job_info)
                        successful_pages += 1
                        self.log(f"第 {page_num} 页爬取成功，共 {len(job_info)} 条数据")
                    else:
                        self.log(f"第 {page_num} 页没有提取到数据")
                else:
                    self.log(f"第 {page_num} 页爬取失败")

                self.progress['value'] = current_progress

                # 在每次请求后随机进行模拟操作
                if page_num < end_page and self.is_running:
                    if random.random() < 0.8:  # 80%的概率在请求后进行模拟操作
                        self.perform_random_operation()
                    else:
                        delay = random.uniform(30, 50)
                        self.log(f"等待 {delay:.2f} 秒后继续下一页...")
                        time.sleep(delay)

            self.update_status(f"爬取完成！成功爬取 {successful_pages} 页数据")

        except Exception as e:
            self.update_status(f"爬取过程中发生错误: {str(e)}")
        finally:
            self.stop_scraping()

    def start_scraping(self):
        """开始爬取"""
        if not self.job_var.get() or not self.city_var.get():
            messagebox.showwarning("输入错误", "请输入职位关键词和城市ID")
            return

        # 验证页码输入
        try:
            start_page = int(self.start_page_var.get())
            end_page = int(self.end_page_var.get())

            if start_page < 1:
                messagebox.showwarning("输入错误", "起始页不能小于1")
                return

            if end_page < start_page:
                messagebox.showwarning("输入错误", "结束页不能小于起始页")
                return

        except ValueError:
            messagebox.showwarning("输入错误", "页码必须是整数")
            return

        self.is_running = True
        self.start_btn.config(state=tk.DISABLED)
        self.stop_btn.config(state=tk.NORMAL)
        self.progress['value'] = 0

        # 在新线程中运行爬取任务
        thread = threading.Thread(target=self.scraping_thread)
        thread.daemon = True
        thread.start()

    def stop_scraping(self):
        """停止爬取"""
        self.is_running = False
        self.start_btn.config(state=tk.NORMAL)
        self.stop_btn.config(state=tk.DISABLED)
        self.update_status("爬取已停止")

        if self.driver:
            try:
                self.driver.quit()
            except:
                pass
            self.driver = None

    def __del__(self):
        """析构函数，确保浏览器被关闭"""
        if self.driver:
            try:
                self.driver.quit()
            except:
                pass


def main():
    root = tk.Tk()
    app = BossZhipinScraper(root)
    root.mainloop()


if __name__ == "__main__":
    main()